Popular Searches
Popular Course Categories
Popular Courses

Reading and processing JSON responses

Reading and processing JSON responses

Flutter APIs & Networking

Reading and Processing JSON Responses in Flutter

Flutter applications frequently communicate with REST APIs and web services. These APIs commonly return data in JSON (JavaScript Object Notation) format. Reading and processing JSON responses means receiving JSON text from an API, decoding it into Dart objects, validating the response, and converting the data into model classes that can be safely used in the Flutter application.

Flutter and Dart provide built-in JSON support through the dart:convert library. The main functions used are jsonDecode() for reading JSON and jsonEncode() for converting Dart data into JSON.

1. What is JSON?

JSON stands for JavaScript Object Notation. It is a lightweight text-based format used to represent structured data. JSON is widely used for communication between Flutter applications and backend servers.

{
  "id": 101,
  "name": "John",
  "email": "[email protected]",
  "isActive": true
}

2. Why JSON is Used in Flutter

  • It is commonly returned by REST APIs.
  • It is easy for humans to read and understand.
  • It can represent objects, arrays, strings, numbers, booleans, and null values.
  • It can be converted into Dart maps and lists.
  • It can be converted into strongly typed Dart model classes.
  • It is useful for sending and receiving data over HTTP.

3. JSON Data Types

JSON Type Example Dart Representation
String "Flutter" String
Number 100 int/num
Decimal 99.50 double/num
Boolean true bool
Null null null
Object {"name":"John"} Map
Array [1,2,3] List

4. Importing dart:convert

The dart:convert library provides JSON encoding and decoding functionality.

import 'dart:convert';

5. What is JSON Decoding?

JSON decoding converts a JSON string received from a server into Dart data structures such as Map and List. The jsonDecode() function parses a JSON string and returns the corresponding Dart representation. A JSON object normally becomes a map, while a JSON array becomes a list.

Example JSON Response

const jsonString = '''
{
  "id": 1,
  "name": "Alice",
  "email": "[email protected]"
}
''';

Decoding the JSON

final data = jsonDecode(jsonString);
print(data);

The result is a Dart map containing the JSON properties.

6. Decoding JSON into Map

For a JSON object, it is common to cast the decoded value to Map.

import 'dart:convert';

const jsonString = '''
{
  "id": 1,
  "name": "Alice",
  "email": "[email protected]"
}
''';

final Map data = jsonDecode(jsonString) as Map;

print(data['id']);
print(data['name']);
print(data['email']);

7. Accessing Values from JSON

After decoding an object, individual values can be accessed using their keys.

final Map user = jsonDecode(jsonString) as Map;

final String name = user['name'] as String;
final String email = user['email'] as String;

print(name);
print(email);

8. Type Casting JSON Values

For safer code, JSON values can be explicitly cast to their expected types.

final Map user = jsonDecode(jsonString) as Map;

final int id = user['id'] as int;
final String name = user['name'] as String;
final String email = user['email'] as String;

print('$id - $name - $email');

9. Reading JSON Arrays

A JSON array is represented using square brackets and becomes a Dart List after decoding.

const jsonString = '''
[
  {
    "id": 1,
    "name": "Alice"
  },
  {
    "id": 2,
    "name": "Bob"
  },
  {
    "id": 3,
    "name": "Charlie"
  }
]
''';

final List users = jsonDecode(jsonString) as List;

print(users[0]);
print(users[1]);
print(users[2]);

10. Processing a JSON List

You can loop through a decoded JSON list to process every object.

final List users = jsonDecode(jsonString) as List;

for (final user in users) {
  print(user['name']);
}

11. Converting JSON List into a Typed List

For better type safety, JSON objects can be converted into a list of model objects.

final List jsonList = jsonDecode(jsonString) as List;

final List<User> users = jsonList
    .map((item) => User.fromJson(item as Map))
    .toList();

12. JSON Object vs JSON Array

JSON Structure Dart Result
{"name":"John"} Map
[{"name":"John"},{"name":"Jane"}] List
"Flutter" String
100 num/int
true bool
null null

13. Reading JSON Responses from an HTTP API

When a Flutter application makes an HTTP request, the server commonly returns an http.Response. The response body is a string. If that body contains JSON, it can be decoded using jsonDecode(). A typical workflow is to check the HTTP status code, decode the response body, and then convert it into a model object.

Required Package

dependencies:
  flutter:
    sdk: flutter
  http: ^1.0.0

Then import the required packages:

import 'dart:convert';
import 'package:http/http.dart' as http;

14. Simple GET Request and JSON Processing

Future<void> fetchUser() async {
  final response = await http.get(
    Uri.parse('https://example.com/api/user/1'),
  );

  if (response.statusCode == 200) {
    final data = jsonDecode(response.body) as Map;

    print(data['name']);
    print(data['email']);
  } else {
    throw Exception('Failed to load user');
  }
}

15. Understanding response.body

The response.body property contains the server response as text. If the server sends JSON, it is usually decoded before accessing individual fields.

final response = await http.get(
  Uri.parse('https://example.com/api/users'),
);

print(response.body);

If the response is JSON, process it with:

final data = jsonDecode(response.body);

16. Checking the HTTP Status Code

Always check the HTTP status code before processing a response as a successful result.

if (response.statusCode == 200) {
  final data = jsonDecode(response.body);
  print(data);
} else {
  throw Exception(
    'Request failed with status: ${response.statusCode}',
  );
}

17. Common HTTP Status Codes

Status Code Meaning
200OK
201Created
204No Content
400Bad Request
401Unauthorized
403Forbidden
404Not Found
500Internal Server Error

18. Creating a Dart Model Class

Using raw maps everywhere can make a larger application difficult to maintain. A model class provides a structured representation of API data and gives better type safety and autocomplete.

class User {
  final int id;
  final String name;
  final String email;

  User({
    required this.id,
    required this.name,
    required this.email,
  });
}

19. Creating a fromJson() Method

A fromJson() factory constructor can convert a decoded JSON map into a Dart object.

class User {
  final int id;
  final String name;
  final String email;

  User({
    required this.id,
    required this.name,
    required this.email,
  });

  factory User.fromJson(Map json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String,
    );
  }
}

20. Processing JSON into a Model Object

final Map jsonMap = jsonDecode(response.body) as Map;

final User user = User.fromJson(jsonMap);

print(user.name);
print(user.email);

21. Why Use Model Classes?

  • Improves type safety.
  • Makes application code easier to understand.
  • Provides IDE autocomplete.
  • Centralizes JSON parsing logic.
  • Reduces repeated map access.
  • Makes API data easier to pass between widgets and services.
  • Makes complex nested responses easier to manage.

22. Complete GET Request with Model Processing

import 'dart:convert';
import 'package:http/http.dart' as http;

class User {
  final int id;
  final String name;
  final String email;

  User({
    required this.id,
    required this.name,
    required this.email,
  });

  factory User.fromJson(Map json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String,
    );
  }
}

Future<User> fetchUser() async {
  final response = await http.get(
    Uri.parse('https://example.com/api/user/1'),
  );

  if (response.statusCode == 200) {
    final jsonMap = jsonDecode(response.body) as Map;
    return User.fromJson(jsonMap);
  }

  throw Exception('Failed to load user');
}

23. Processing a JSON List into Model Objects

Many APIs return an array of objects rather than a single object.

Future<List<User>> fetchUsers() async {
  final response = await http.get(
    Uri.parse('https://example.com/api/users'),
  );

  if (response.statusCode == 200) {
    final List jsonList = jsonDecode(response.body) as List;

    return jsonList
        .map(
          (item) => User.fromJson(item as Map),
        )
        .toList();
  }

  throw Exception('Failed to load users');
}

24. Processing Nested JSON

API responses can contain objects inside other objects.

{
  "id": 1,
  "name": "John",
  "address": {
    "city": "Mumbai",
    "country": "India"
  }
}

Address Model

class Address {
  final String city;
  final String country;

  Address({
    required this.city,
    required this.country,
  });

  factory Address.fromJson(Map json) {
    return Address(
      city: json['city'] as String,
      country: json['country'] as String,
    );
  }
}

User Model with Nested Address

class User {
  final int id;
  final String name;
  final Address address;

  User({
    required this.id,
    required this.name,
    required this.address,
  });

  factory User.fromJson(Map json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      address: Address.fromJson(
        json['address'] as Map,
      ),
    );
  }
}

25. Processing Nested JSON Arrays

Consider an API response containing a user and a list of orders.

{
  "id": 1,
  "name": "John",
  "orders": [
    {
      "id": 101,
      "product": "Laptop"
    },
    {
      "id": 102,
      "product": "Phone"
    }
  ]
}

Order Model

class Order {
  final int id;
  final String product;

  Order({
    required this.id,
    required this.product,
  });

  factory Order.fromJson(Map json) {
    return Order(
      id: json['id'] as int,
      product: json['product'] as String,
    );
  }
}

User Model

class User {
  final int id;
  final String name;
  final List<Order> orders;

  User({
    required this.id,
    required this.name,
    required this.orders,
  });

  factory User.fromJson(Map json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      orders: (json['orders'] as List)
          .map(
            (item) => Order.fromJson(
              item as Map,
            ),
          )
          .toList(),
    );
  }
}

26. Handling Nullable JSON Fields

API fields may sometimes be null. Your Dart model should reflect that possibility.

{
  "id": 1,
  "name": "John",
  "phone": null
}

The corresponding model can use a nullable type:

class User {
  final int id;
  final String name;
  final String? phone;

  User({
    required this.id,
    required this.name,
    this.phone,
  });

  factory User.fromJson(Map json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      phone: json['phone'] as String?,
    );
  }
}

27. Handling Missing JSON Fields

Sometimes an API may not return an optional field. You can provide a default value when appropriate.

final String name = json['name'] as String? ?? 'Unknown';

For example:

factory User.fromJson(Map json) {
  return User(
    id: json['id'] as int,
    name: json['name'] as String? ?? 'Unknown',
    email: json['email'] as String? ?? '',
  );
}

28. Handling Invalid JSON

If the response body is not valid JSON, JSON decoding can throw a FormatException.

try {
  final data = jsonDecode(response.body);
  print(data);
} on FormatException {
  print('Invalid JSON response');
}

29. Complete Error Handling

Future<User> fetchUser() async {
  try {
    final response = await http.get(
      Uri.parse('https://example.com/api/user/1'),
    );

    if (response.statusCode != 200) {
      throw Exception(
        'Server returned ${response.statusCode}',
      );
    }

    final data = jsonDecode(response.body) as Map;
    return User.fromJson(data);
  } on FormatException {
    throw Exception('Invalid JSON received from server');
  } catch (e) {
    throw Exception('Failed to process response: $e');
  }
}

30. Using JSON with FutureBuilder

Because HTTP requests are asynchronous, Flutter applications can use FutureBuilder to display loading, success, and error states.

FutureBuilder<User>(
  future: fetchUser(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const CircularProgressIndicator();
    }

    if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    }

    if (!snapshot.hasData) {
      return const Text('No user found');
    }

    final user = snapshot.data!;

    return Text(
      '${user.name} - ${user.email}',
    );
  },
)

31. Reading JSON and Displaying It in a ListView

FutureBuilder<List<User>>(
  future: fetchUsers(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }

    if (snapshot.hasError) {
      return Center(
        child: Text('Error: ${snapshot.error}'),
      );
    }

    final users = snapshot.data ?? [];

    return ListView.builder(
      itemCount: users.length,
      itemBuilder: (context, index) {
        final user = users[index];

        return ListTile(
          title: Text(user.name),
          subtitle: Text(user.email),
        );
      },
    );
  },
)

32. JSON Encoding

JSON encoding is the opposite of JSON decoding. It converts a Dart object such as a map or list into a JSON string. Dart provides jsonEncode() for this purpose.

final user = {
  'name': 'John',
  'email': '[email protected]',
};

final jsonString = jsonEncode(user);

print(jsonString);

33. Sending JSON in a POST Request

When sending JSON to a server, convert the Dart map to a JSON string using jsonEncode() and set the appropriate content type.

Future<void> createUser() async {
  final user = {
    'name': 'John',
    'email': '[email protected]',
  };

  final response = await http.post(
    Uri.parse('https://example.com/api/users'),
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(user),
  );

  if (response.statusCode == 201) {
    print('User created successfully');
  } else {
    print('Failed to create user');
  }
}

34. Adding toJson() to a Model

A model can provide a toJson() method to convert itself into a JSON-compatible map.

class User {
  final int id;
  final String name;
  final String email;

  User({
    required this.id,
    required this.name,
    required this.email,
  });

  factory User.fromJson(Map json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
    };
  }
}

35. Encoding a Model Object

After defining toJson(), the model can be converted into JSON.

final user = User(
  id: 1,
  name: 'John',
  email: '[email protected]',
);

final jsonString = jsonEncode(user.toJson());

print(jsonString);

36. Complete JSON Processing Flow

  1. Send an HTTP request.
  2. Receive the HTTP response.
  3. Check the HTTP status code.
  4. Read response.body.
  5. Decode the JSON using jsonDecode().
  6. Convert the decoded map into a model using fromJson().
  7. Use the model object inside the Flutter UI.
HTTP Request
     ↓
Server Response
     ↓
response.body
     ↓
jsonDecode()
     ↓
Map / List
     ↓
fromJson()
     ↓
Dart Model
     ↓
Flutter UI

37. Manual JSON Processing

For small applications, JSON can be processed manually using jsonDecode() and map access.

final data = jsonDecode(response.body) as Map;

print(data['name']);
print(data['email']);

This approach is simple and requires no additional serialization package.

38. Model-Based JSON Processing

For larger applications, model classes provide a cleaner structure.

final data = jsonDecode(response.body) as Map;

final user = User.fromJson(data);

print(user.name);
print(user.email);

This approach keeps JSON parsing logic inside the model and reduces repeated map access in application code.

39. Manual vs Model-Based Processing

Feature Manual Map Model Class
Easy for small examples Yes Yes
Type safety Limited Better
Autocomplete Limited Better
Reusable parsing Limited Yes
Nested data Can become difficult Easier to organize
Large application Harder to maintain More maintainable

40. Processing API Responses with Separate Service Classes

In a larger Flutter application, API communication can be separated from UI code.

class UserService {
  Future<User> fetchUser() async {
    final response = await http.get(
      Uri.parse('https://example.com/api/user/1'),
    );

    if (response.statusCode != 200) {
      throw Exception('Failed to load user');
    }

    final data = jsonDecode(response.body) as Map;

    return User.fromJson(data);
  }
}

41. Service, Model, and UI Architecture

Flutter UI
    ↓
Service / Repository
    ↓
HTTP Request
    ↓
API Server
    ↓
JSON Response
    ↓
jsonDecode()
    ↓
Model.fromJson()
    ↓
Model Object
    ↓
Flutter UI

42. Handling API Response Objects with a Data Wrapper

Some APIs wrap their actual data inside another object.

{
  "success": true,
  "message": "Users fetched successfully",
  "data": [
    {
      "id": 1,
      "name": "John"
    },
    {
      "id": 2,
      "name": "Jane"
    }
  ]
}

The data can first be extracted from the wrapper.

final json = jsonDecode(response.body) as Map;

final bool success = json['success'] as bool;
final String message = json['message'] as String;

final List data = json['data'] as List;

print(success);
print(message);
print(data.length);

43. Processing API Data Safely

When working with external APIs, do not assume that every field will always exist or have the expected value. Validate important fields and handle missing or invalid data appropriately.

final data = jsonDecode(response.body) as Map;

final name = data['name'];

if (name is String) {
  print(name);
} else {
  print('Invalid name value');
}

44. JSON Response Error Object

An API may return JSON even when the request fails.

{
  "success": false,
  "message": "Invalid credentials",
  "code": "AUTH_ERROR"
}

You should check the HTTP status and, when appropriate, inspect the JSON error payload.

if (response.statusCode != 200) {
  final errorData = jsonDecode(response.body) as Map;

  final message =
      errorData['message'] as String? ?? 'Unknown error';

  throw Exception(message);
}

45. Handling Empty Responses

Some endpoints may return an empty body, especially for operations that do not return data. Avoid blindly calling jsonDecode() on an empty response.

if (response.body.isNotEmpty) {
  final data = jsonDecode(response.body);
  print(data);
} else {
  print('Response body is empty');
}

46. JSON Field Naming

API field names may use naming conventions such as snake_case while Dart properties commonly use camelCase.

{
  "first_name": "John",
  "last_name": "Smith",
  "profile_image": "profile.jpg"
}

The model can map these API fields manually:

class User {
  final String firstName;
  final String lastName;
  final String profileImage;

  User({
    required this.firstName,
    required this.lastName,
    required this.profileImage,
  });

  factory User.fromJson(Map json) {
    return User(
      firstName: json['first_name'] as String,
      lastName: json['last_name'] as String,
      profileImage: json['profile_image'] as String,
    );
  }
}

47. Large JSON Responses

Large JSON responses can require more processing time and memory. For large datasets or files, JSON parsing can be moved away from the main UI work when appropriate so that the interface remains responsive.

48. Using compute() for Heavy JSON Parsing

For CPU-intensive parsing, Flutter applications can move parsing work to another isolate using appropriate asynchronous techniques.

import 'dart:convert';
import 'package:flutter/foundation.dart';

List<User> parseUsers(String responseBody) {
  final List jsonList = jsonDecode(responseBody) as List;

  return jsonList
      .map(
        (item) => User.fromJson(
          item as Map,
        ),
      )
      .toList();
}

Future<List<User>> parseUsersInBackground(
  String responseBody,
) async {
  return compute(parseUsers, responseBody);
}

49. JSON Serialization and Deserialization

Operation Direction Common Function
Decoding JSON String → Dart data jsonDecode()
Deserialization JSON Map → Dart Model fromJson()
Encoding Dart data → JSON String jsonEncode()
Serialization Dart Model → JSON-compatible data toJson()

50. Common Mistakes While Processing JSON

  • Forgetting to import dart:convert.
  • Calling jsonDecode() on invalid JSON.
  • Assuming every API response is a JSON object.
  • Assuming an array response is a map.
  • Not checking the HTTP status code.
  • Using non-nullable fields when the API can return null.
  • Using incorrect JSON field names.
  • Ignoring nested JSON structures.
  • Not handling API errors.
  • Putting all JSON parsing logic directly inside widgets.
  • Parsing very large JSON payloads synchronously when background processing is more appropriate.

51. Best Practices for Reading JSON Responses

  1. Always check the HTTP status code.
  2. Decode JSON using jsonDecode().
  3. Use model classes for reusable API data.
  4. Create fromJson() methods for deserialization.
  5. Create toJson() methods when data must be sent back to an API.
  6. Handle nullable and optional fields.
  7. Handle malformed JSON and network errors.
  8. Keep API communication outside the UI when possible.
  9. Use typed collections for better readability.
  10. Consider background parsing for sufficiently large JSON payloads.
  11. Test model serialization and deserialization.

52. Complete Example: Reading JSON from an API

import 'dart:convert';
import 'package:http/http.dart' as http;

class User {
  final int id;
  final String name;
  final String email;

  User({
    required this.id,
    required this.name,
    required this.email,
  });

  factory User.fromJson(Map json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String,
    );
  }
}

Future<User> fetchUser() async {
  final response = await http.get(
    Uri.parse('https://example.com/api/user/1'),
    headers: {
      'Accept': 'application/json',
    },
  );

  if (response.statusCode == 200) {
    try {
      final data = jsonDecode(response.body) as Map;
      return User.fromJson(data);
    } on FormatException {
      throw Exception('Server returned invalid JSON');
    }
  }

  throw Exception(
    'Failed to load user. Status: ${response.statusCode}',
  );
}

53. Complete Example: Displaying the Processed JSON in Flutter

import 'package:flutter/material.dart';

class UserScreen extends StatelessWidget {
  const UserScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('User'),
      ),
      body: FutureBuilder<User>(
        future: fetchUser(),
        builder: (context, snapshot) {
          if (snapshot.connectionState ==
              ConnectionState.waiting) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }

          if (snapshot.hasError) {
            return Center(
              child: Text(
                'Error: ${snapshot.error}',
              ),
            );
          }

          if (!snapshot.hasData) {
            return const Center(
              child: Text('No user data'),
            );
          }

          final user = snapshot.data!;

          return Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment:
                  CrossAxisAlignment.start,
              children: [
                Text('ID: ${user.id}'),
                Text('Name: ${user.name}'),
                Text('Email: ${user.email}'),
              ],
            ),
          );
        },
      ),
    );
  }
}

54. JSON Processing Architecture Example

API Server
    ↓
HTTP GET Request
    ↓
http.Response
    ↓
Check statusCode
    ↓
response.body
    ↓
jsonDecode()
    ↓
Map
    ↓
User.fromJson()
    ↓
User Object
    ↓
FutureBuilder
    ↓
Flutter Widget

55. Quick Revision

Concept Purpose
dart:convert Provides JSON conversion utilities
jsonDecode() Converts JSON text into Dart data
jsonEncode() Converts Dart data into JSON text
fromJson() Creates a Dart model from JSON data
toJson() Converts a Dart model into JSON-compatible data
response.body Contains the HTTP response body
response.statusCode Indicates the HTTP response status
FutureBuilder Displays asynchronous API results in Flutter UI

56. Interview Questions

  1. What is JSON?
  2. Why is JSON commonly used with Flutter APIs?
  3. Which Dart library is used for JSON encoding and decoding?
  4. What does jsonDecode() do?
  5. What does jsonEncode() do?
  6. What happens when a JSON object is decoded?
  7. What happens when a JSON array is decoded?
  8. What is the purpose of fromJson()?
  9. What is the purpose of toJson()?
  10. Why should HTTP status codes be checked before processing a response?
  11. How do you process a JSON list in Flutter?
  12. How do you process nested JSON?
  13. How do you handle nullable JSON fields?
  14. What exception can occur when JSON is invalid?
  15. Why are model classes useful for API responses?
  16. How can large JSON responses be processed without blocking the UI?

57. Practice Exercise

Create a Flutter application that reads the following JSON response and displays the users in a ListView.

[
  {
    "id": 1,
    "name": "Rahul",
    "email": "[email protected]"
  },
  {
    "id": 2,
    "name": "Priya",
    "email": "[email protected]"
  },
  {
    "id": 3,
    "name": "Amit",
    "email": "[email protected]"
  }
]

Tasks

  1. Create a User model.
  2. Create a fromJson() constructor.
  3. Decode the JSON using jsonDecode().
  4. Convert every JSON object into a User.
  5. Store the users in a List.
  6. Display the users using ListView.builder.
  7. Add error handling for invalid JSON.

58. Official Learning Resources

59. Learn Flutter with JustAcademy

For structured Flutter learning, explore the following resources:

60. Summary

Reading and processing JSON responses is an essential skill for Flutter application development. A typical API workflow starts with an HTTP request, receives a response, checks the status code, reads the response body, decodes JSON using jsonDecode(), and converts the decoded data into Dart model objects using fromJson(). For sending data to an API, Dart uses jsonEncode() and model classes can provide toJson(). For production applications, proper error handling, nullable fields, nested models, typed collections, and appropriate JSON parsing architecture help make API-driven Flutter applications easier to maintain and more reliable.

Previous Next
whatsapp